using System;
using System.Collections.Generic;
using System.Text;
using Nojin.Netwwork.Download;
using System.ComponentModel;
using System.IO;
using System.Xml.Serialization;
using System.Diagnostics;
using System.Configuration;
using Nojin.Deployment;

namespace Nojin.Deployment
{
    /// <summary>
    /// Downloads and install new version based on specified <seealso cref="UpdateInfo"/> and <seealso cref="DeploymentManifest"/>    
    /// <remarks>
    /// The main method is <seealso cref="Run"/> which starts the process.
    /// To control the download process you could use <seealso cref="DownloadProgressReport"/> event.
    /// By default after downloading the new version it will be installed. To control this and prevents the installation in
    /// some circumstances you could handle the <seealso cref="InstallReady"/> event, it has a Cancel property which you could set it.
    /// To become notified about when download is completed or installation completed handle these events appropriately: <seealso cref="DownloadCompleted"/> and <seealso cref="InstallCompleted"/>.
    /// It will save its informaion to be recoverable after program re-execution.
    /// </remarks>
    /// </summary>
    public class ApplicationDeploymentProcess
    {

        #region Fields
        ApplicationDeploymentProcessState state;
        string tempPath = "NewVersion";
        bool newUpdate = false;
        #endregion

        #region Constructors
        /// <summary>
        /// Constructs a new instance of <seealso cref="ApplicationDeploymentProcess"/> class.
        /// </summary>        
        /// <param name="tempPath">The relative name of directory to dopy pre-install downloaded files.</param>
        /// <param name="updateInfo">Information about new version as a <seealso cref="UpdateInfo"/> object.</param>
        /// <param name="manifest">Information required to download and install new version as a <seealso cref="DeploymentManifest"/> object.</param>
        public ApplicationDeploymentProcess(string tempPath, UpdateInfo updateInfo, DeploymentManifest manifest)
        {
            this.Initialize(tempPath, updateInfo, manifest);
        } 
        #endregion

        #region Properties
        public bool NewUpdate
        {
            get
            {
                return newUpdate;
            }
        }
        public int TotalDownloadingFiles
        {
            get
            {
                return state.Manifest.Downloads.Count;
            }
        }
        public bool IsDownloadCompleted
        {
            get
            {
                return state.DownloadedFiles.Count == state.Manifest.Downloads.Count;
            }
        }
        #endregion

        #region Methods

        /// <summary>
        /// Starts or resumes deployment process.
        /// </summary>
        public void Run()
        {
            downloads = state.Manifest.Downloads.GetEnumerator();
            downloads.Reset();

            // Note that belove method call cause all of these:
            // iterates through downloads list and sequentially downloads any remained download and
            // when no download remained starts the install progress.

            DownloadNext();
        }

        private void Install()
        {
            // The download progress downloads the files to a temporary path,
            // this method will copy the downloaded files from that temporary location
            // to install target path. but some files are zipped, so they should be 
            // extracted to target path. also some files are executables which should be
            // executed.
            // The target install directory is a new directory specific to new version
            // and each update only contains files with new version, so this method
            // will copy the files from the old version directory to new version's directory.



            #region Copy files from old version's folder to new version's folder
            Configuration launcherConfig = LauncherHelperMethods.GetLauncherConfig();
            string currentInstallDirectoryPath = launcherConfig.AppSettings.Settings[Constants.TargetVersionConfigKey].Value;
            currentInstallDirectoryPath = Path.Combine(LauncherHelperMethods.GetLauncherPath(), currentInstallDirectoryPath);

            DirectoryInfo currentInstallDirectory = new DirectoryInfo(currentInstallDirectoryPath);
            if (currentInstallDirectory.Exists)
            {
                FileInfo[] oldFiles = currentInstallDirectory.GetFiles();
                foreach (FileInfo oldFile in oldFiles)
                {
                    CopyToInstallDirectory(oldFile.FullName, "", false);
                }
            }
            
            #endregion


            #region Copy or extract each downloaded file to target install directory or perform other designated operations.
            foreach (DownloadEntry enrty in state.Manifest.Downloads)
            {
                switch (enrty.InstallAction)
                {
                    case InstallAction.Copy:
                        CopyToInstallDirectory(GetDownloadTemporaryPath(enrty), enrty.TargetPath, true);
                        break;
                    case InstallAction.Run:
                        Process.Start(GetDownloadTemporaryPath(enrty));
                        break;
                    case InstallAction.UnzipCopy:
                        UnzipToInstallDirectory(GetDownloadTemporaryPath(enrty), enrty.TargetPath);
                        break;
                    case InstallAction.UnzipRun:

                        break;
                }
            }
            
            #endregion
            
            UpdateBootStrapperConfig();

            Directory.Delete(GetDownloadTemporaryPath(), true);

            if (InstallCompleted != null)
                InstallCompleted(this, EventArgs.Empty);
        }

        public long GetTotalDownloadedSize()
        {
            long downloadedSize = 0;
            foreach (DownloadEntry entry in state.Manifest.Downloads)
            {
                string fileLocalPath = GetDownloadTemporaryPath(entry);
                // Check if the file exists on hard, so it include the partial downloaded files.                
                if (File.Exists(fileLocalPath))
                {
                    downloadedSize += new FileInfo(fileLocalPath).Length;
                }
            }
            return downloadedSize;
        }

        public void SaveState()
        {
            string filePath = Path.Combine(GetDownloadTemporaryPath(), "state.sav");
            ApplicationDeploymentProcessState.Save(filePath, this.state);
        }

        public void LoadState()
        {
            string filePath = Path.Combine(GetDownloadTemporaryPath(), "state.sav");
            if (File.Exists(filePath))
                this.state = ApplicationDeploymentProcessState.Load(filePath);
        }

        private void Initialize(string tempPath, UpdateInfo updateInfo, DeploymentManifest manifest)
        {
            this.tempPath = tempPath;

            LoadState(); // If any state exists it will have a value and it mean that an old process exists.

            if (state == null || state.UpdateInfo == null ||
                this.state.UpdateInfo.Version != updateInfo.Version) // if there is no old process or it is for a previous version
            {                
                // start the process from begin and as a new update
                this.state = new ApplicationDeploymentProcessState(updateInfo, manifest);
                newUpdate = true;
            }
            else
            {
                // resume the old saved process
                this.state.UpdateInfo = updateInfo;
                this.state.Manifest = manifest;
                newUpdate = false;
            }

            if (newUpdate)
            {                
                // if this is a new update and the temporary download path exists, it may 
                // make downloads to that folder corrupted.
                // so we delete the directory and every files within it.

                string targetPath = GetDownloadTemporaryPath();
                if (Directory.Exists(targetPath))
                    Directory.Delete(targetPath);
            }
        }

        private void CheckInstallDirectoryExists()
        {
            string installPath = GetInstallPath();
            if (!Directory.Exists(installPath))
            {
                Directory.CreateDirectory(installPath);
            }
        }

        private string GetInstallPath()
        {
            string installPath = LauncherHelperMethods.GetLauncherPath();
            installPath = Path.Combine(installPath, state.UpdateInfo.Version.ToString());
            return installPath;
        }

        private string GetDownloadTemporaryPath(DownloadEntry download)
        {
            string targetPath = GetDownloadTemporaryPath();
            if (!string.IsNullOrEmpty(download.TargetPath))
                targetPath = Path.Combine(targetPath, download.TargetPath);
            targetPath = Path.Combine(targetPath, Path.GetFileName(download.Uri));
            return targetPath;
        }

        private string GetDownloadTemporaryPath()
        {
            string targetPath = LauncherHelperMethods.GetLauncherPath();
            targetPath = Path.Combine(targetPath, tempPath);
            return targetPath;
        }

        IEnumerator<DownloadEntry> downloads;
        private void DownloadNext()
        {
            bool nextIsAvailable = downloads.MoveNext();
            if (nextIsAvailable)
            {
                DownloadEntry download = downloads.Current;
                
                // Don't download downloaded files again.
                if (state.DownloadedFiles.Contains(download.Uri))
                {
                    DownloadNext();
                    return;
                }
                
                #region Download file asynchorn
                string targetPath = GetDownloadTemporaryPath(download);
                FileDownloader fileDownloader = new FileDownloader();
                fileDownloader.DownloadProgressChanged += new EventHandler<FileDownloadProgressChangedEventArgs>(fileDownloader_DownloadProgressChanged);
                fileDownloader.DownloadCompleted += new EventHandler<FileDownloadCompletedEventArgs>(fileDownloader_DownloadCompleted);
                fileDownloader.DownloadAsync(download.Uri, Path.GetDirectoryName(targetPath));
                
                #endregion            

                // After asynchron download completed, this method(DownloadNext) will be called again.

            }
            else // All files are downloaded
            {                

                if (DownloadCompleted != null)
                    DownloadCompleted(this, EventArgs.Empty);

                if (InstallReady != null)
                {
                    CancelEventArgs cancelEventArgs = new CancelEventArgs();
                    InstallReady(this, cancelEventArgs);
                    if (!cancelEventArgs.Cancel)
                    {
                        Install();
                    }
                }
                else
                    Install();
            }
        }

        private void fileDownloader_DownloadCompleted(object sender, FileDownloadCompletedEventArgs e)
        {
            if (e.Canceled || e.Error != null)
            {
            }
            else
            {
                DownloadEntry download = downloads.Current;
                state.DownloadedFiles.Add(download.Uri);
                SaveState();
                DownloadNext();
            }
        }

        private void fileDownloader_DownloadProgressChanged(object sender, FileDownloadProgressChangedEventArgs e)
        {
            DownloadEntry download = downloads.Current;

            if (DownloadProgressReport != null)
            {
                ProgressReportEventArgs eventArgs = new ProgressReportEventArgs(download.Uri, state.Manifest.Downloads.Count, state.DownloadedFiles.Count + 1, e.BytesReceived, e.TotalBytesToReceive);
                DownloadProgressReport(this, eventArgs);
                if (eventArgs.Cancel)
                {
                    FileDownloader downloader = sender as FileDownloader;
                    downloader.DownloadAsyncCancel();
                }
            }
        }

        private void CopyToInstallDirectory(string filePath, string targetPath, bool deleteOldFile)
        {
            CheckInstallDirectoryExists();
            string copyTarget = GetInstallPath();
            if (!string.IsNullOrEmpty(targetPath))
                copyTarget = Path.Combine(copyTarget, targetPath);
            copyTarget = Path.Combine(copyTarget, Path.GetFileName(filePath));
            if (File.Exists(copyTarget))
                File.Delete(copyTarget);
            File.Copy(filePath, copyTarget);
            if ( deleteOldFile )
                File.Delete(filePath);
        }
        
        private void UnzipToInstallDirectory(string filePath, string targetPath)
        {
            string copyTarget = GetInstallPath();
            if (!string.IsNullOrEmpty(targetPath))
                copyTarget = Path.Combine(copyTarget, targetPath);
            ZipHelp.UnZip(filePath, copyTarget, 64 * 1024);
            File.Delete(filePath);
        }

        private void UpdateBootStrapperConfig()
        {
            Configuration launcherConfig = LauncherHelperMethods.GetLauncherConfig();
            launcherConfig.AppSettings.Settings[Constants.TargetVersionConfigKey].Value = state.UpdateInfo.Version.ToString();
            launcherConfig.Save();
        }

        #endregion

        #region Events

        public event EventHandler<ProgressReportEventArgs> DownloadProgressReport;

        public event EventHandler DownloadCompleted;

        public event CancelEventHandler InstallReady;

        public event EventHandler InstallCompleted;

        #endregion

    }

    public class LauncherHelperMethods
    {

        /// <summary>
        /// Gets path to lanucher file.
        /// Usually it is the parent directory of executing application's directory.
        /// </summary>
        /// <remarks>
        /// Launcher is a simple application that is responsible for launching last version of application.
        /// It will be automatically configured with AutoUpdate component to point to last version.
        /// </remarks>
        /// <returns>Path to lanucher file.</returns>
        public static string GetLauncherPath()
        {
            string currentDirectoryPath = Environment.CurrentDirectory;
            DirectoryInfo currentDirectory = new DirectoryInfo(currentDirectoryPath);
            return currentDirectory.Parent.FullName;
        }

        /// <summary>
        /// Returns the Configuration object for launcher application.
        /// </summary>
        /// <remarks>
        /// Launcher is a simple application that is responsible for launching last version of application.
        /// It will be automatically configured with AutoUpdate component to point to last version.
        /// </remarks>
        /// <returns>Configuration object for launcher application.</returns>
        public static Configuration GetLauncherConfig()
        {
            Configuration launcherConfig = System.Configuration.ConfigurationManager.OpenExeConfiguration(Path.Combine(GetLauncherPath(), Constants.LauncherApplicationName));
            return launcherConfig;
        }
    }

    public class ProgressReportEventArgs : CancelEventArgs
    {
        #region Constructors
        public ProgressReportEventArgs(string currentFileName,int allFilesCount,int currentFileNumber, long bytesReceived, long totalBytesToReceive)
        {            
            this.currentFileName = currentFileName;
            this.allFilesCount = allFilesCount;
            this.currentFileNumber = currentFileNumber;
            this.bytesReceived = bytesReceived;
            this.totalBytesToReceive = totalBytesToReceive;
        }
        public ProgressReportEventArgs(string currentFileName, int allFilesCount,int currentFileNumber, long bytesReceived, long totalBytesToReceive, bool cancel)
            : base(cancel)
        {
            this.currentFileName = currentFileName;
            this.allFilesCount = allFilesCount;
            this.currentFileNumber = currentFileNumber;
            this.bytesReceived = bytesReceived;
            this.totalBytesToReceive = totalBytesToReceive;
        }
        #endregion

        #region Properties
        string currentFileName;
        public string CurrentFileName
        {
            get { return currentFileName; }
            set { currentFileName = value; }
        }
        int currentFileNumber;
        public int CurrentFileNumber
        {
            get
            {
                return currentFileNumber;
            }
        }        
        int allFilesCount;
        public int AllFilesCount
        {
            get
            {
                return allFilesCount;
            }            
        }
        long bytesReceived;
        public long BytesReceived
        {
            get { return bytesReceived; }
            set { bytesReceived = value; }
        }
        long totalBytesToReceive;
        public long TotalBytesToReceive
        {
            get { return totalBytesToReceive; }
            set { totalBytesToReceive = value; }
        }
        #endregion
    }

    public class ApplicationDeploymentProcessState
    {
        #region Constructors
        public ApplicationDeploymentProcessState()
        {
            this.downloadedFiles = new List<string>();
        }
        public ApplicationDeploymentProcessState(UpdateInfo updateInfo, DeploymentManifest manifest)
            : this()
        {
            this.updateInfo = updateInfo;
            this.manifest = manifest;
        }
        #endregion

        #region Properties
        UpdateInfo updateInfo;
        public UpdateInfo UpdateInfo
        {
            get { return updateInfo; }
            set { updateInfo = value; }
        }

        DeploymentManifest manifest;
        public DeploymentManifest Manifest
        {
            get { return manifest; }
            set { manifest = value; }
        }

        List<string> downloadedFiles;
        public List<string> DownloadedFiles
        {
            get { return downloadedFiles; }
        }

        //List<string> incompleteDownloads;
        //public List<string> IncompleteDownloads
        //{
        //    get { return incompleteDownloads; }
        //}
        #endregion

        #region Methods
        public static ApplicationDeploymentProcessState Load(string filePath)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(ApplicationDeploymentProcessState));
            using (FileStream fileStream = new FileStream(filePath, FileMode.Open))
            {
                ApplicationDeploymentProcessState state = (ApplicationDeploymentProcessState)serializer.Deserialize(fileStream);
                fileStream.Close();
                return state;
            }
        }

        public static void Save(string filePath, ApplicationDeploymentProcessState state)
        {
            string directoryName = Path.GetDirectoryName(filePath);
            if (!Directory.Exists(directoryName))
                Directory.CreateDirectory(directoryName);

            XmlSerializer serializer = new XmlSerializer(typeof(ApplicationDeploymentProcessState));
            using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
            {
                serializer.Serialize(fileStream, state);
                fileStream.Close();
            }
        }
        #endregion
    }

}
